//使用信号量的方式解决生产者消费者问题，一个线程想要访问某个资源要现申请信号量
//利用这种信号量的机制来完成该模型的同步互斥等要求

//由于加上了信号量来对我们的生产者和消费者线程进行控制，所以生产者和消费者线程可以使用不同的锁来限制，不用怕生产者线程和消费者线程操作到了同一块资源空间
//例如：消费者有信号量的时候一定是因为生产者已经生成完成了数据，此时我们消费者线程可以先把所有的信号量申请完，申请到信号量的线程串行的去处理数据不会造成数据的错乱，并且由于处理的是生产者线程已经走过的位置，所以不会对生产者线程产生影响，所以通过信号量我们允许生产者和消费者线程二者之间的并发执行，二者可以拥有自己的锁
//基于信号量方式解决cp问题时，生产者和消费者线程互相动态的操作对方和自己的信号量，来完成线程之间的协同配合
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<ctime>
#include"Task.hpp"
#include"RingQueue.hpp"
using namespace std;

struct ThreadData//把环形队列和该线程的名字封装在一起，注意：所有对象的该结构体中的环形队列指针都指向同一个环形队列，只有name是不同的
{
    RingQueue<Task>*rq;
    string threadname;
};
//消费者：
void *Consumer(void *args)
{
    ThreadData*td=static_cast<ThreadData*>(args);
    RingQueue<Task>*rq=td->rq;
    string name=td->threadname;
    delete td;//释放主线程申请的空间，注意这并不会释放掉我们的环形队列，不用担心，因为在结构体中的只是指向环形队的指针，并不是环形队成员，只是该指针变量释放掉了而已，与指针指向的对象无关
    //解释：当一个对象被销毁时，它的成员对象（非指针类型）会先于对象本身的析构函数被调用。这是自动发生的，不需要程序员显式调用。
    while(true)
    {
        //消费：
        Task t;
        rq->pop(&t);//输出型参数，把要处理的任务输出到t中

        //处理数据：
        //线程在处理数据的时候是不占用锁的，模拟了对数据的处理，所以对数据的处理是并发的，所以整体看该模型是高效的
        t();
        cout<<"处理任务："<<t.GetTask()<<"运算结果是："<<t.GetResult()<<"name:"<<name<<endl;
    }
    return nullptr;
}
//生产者：
void *Productor(void *args)
{
    int len=opers.size();
    ThreadData*td=static_cast<ThreadData*>(args);
    RingQueue<Task>*rq=td->rq;
    string name=td->threadname;
    delete td;//释放主线程申请的空间
    while(true)
    {
        //模拟生产者产生数据：
        int data1=rand()%10+1;
        usleep(10);
        int data2=rand()%10;
        char op=opers[rand()%len];
        Task t(data1,data2,op);
        //产生数据的过程也是并发的，不受锁的约束

        //生产：
        rq->push(t);
        cout<<"生产了一个任务："<<t.GetTask()<<"who:"<<name<<endl;
        //让生产者慢一点：
        sleep(1);
    }
    return nullptr;
}
int main()
{
    srand(time(nullptr)^getpid());
    RingQueue<Task>*rq=new RingQueue<Task>(50);
    pthread_t c[3];
    pthread_t p[5];
    //创建多个消费者，生产者线程：
    for(int i=0;i<5;i++)
    {
        ThreadData *td=new ThreadData();
        td->rq=rq;//所有的线程的环形队列都是指向同一个
        td->threadname="Consumer-"+to_string(i);
        pthread_create(c+i,nullptr,Consumer,td);//c是数组首地址，也就是只想数组首地址的指针，c+i很巧妙地把对应变量的地址进行了传参
    }
    for(int i=0;i<3;i++)
    {
        ThreadData *td=new ThreadData();
        td->rq=rq;//所有的线程的环形队列都是指向同一个
        td->threadname="Productor-"+to_string(i);
        pthread_create(p+i,nullptr,Productor,td);
    }
    //对所有线程释放：（阻塞等待的方式）
    for(int i=0;i<5;i++)
    {
        pthread_join(c[i],nullptr);
    }
    for(int i=0;i<3;i++)
    {
        pthread_join(p[i],nullptr);
    }
    delete rq;
    return 0;
}

//一个小的注意点：关于cp问题的代码在运行时，打印总会时不时出现一些顺序错乱的情况，比如先打印处理任务才打印生产了这个任务，这并不是代逻辑错误：
//解释：顺序错乱的原因主要是由于多个线程同时访问和操作共享资源（在这种情况下是标准输出流 cout）而没有适当的同步措施。这属于并发编程中典型的同步问题，并不是代码逻辑错误，而是由于输入输出（I/O）操作在多线程环境下的线程安全问题。
/*原理解析：
一，为什么会出现顺序错乱？
1，共享资源竞争：
当多个线程尝试同时向标准输出流写入数据时，由于 cout 是一个共享资源，没有互斥保护，所以输出可能会被中断或重叠，导致输出内容的顺序与预期不一致。

2，缓冲区行为：
标准输出流 cout 是带缓冲的。这意味着输出可能不会立即显示在控制台上，而是先存储在缓冲区中。不同线程对缓冲区的并发访问可能导致输出内容的交错。

3，线程调度：
线程的调度是由操作系统管理的，线程执行的顺序和时间是不确定的。因此，即使程序逻辑正确，线程的执行顺序也可能导致输出顺序错乱。

二，如何解决顺序错乱的问题？
1，使用互斥锁：,
可以通过在输出操作前后加锁来确保每次只有一个线程可以向 cout 写入数据。这可以通过 mutex 实现，如前所述。

2，使用条件变量：
如果需要保持输出的特定顺序，可以使用条件变量来控制线程执行的顺序。

3，使用线程安全的输出流：
C++11 标准库提供了线程安全的输出流，例如 std::ostringstream，可以用来避免多线程输出时的冲突。

4，减少共享资源的访问：
尽量减少多个线程对共享资源（如 cout）的直接访问，或者确保每次只有一个线程可以访问共享资源。
三，结论：
顺序错乱的问题主要是由于多线程环境下对共享资源（标准输出流）的并发访问没有适当的同步措施导致的。这不是代码逻辑错误，而是并发编程中的一个常见问题。通过引入适当的同步机制，可以解决这个问题，确保输出的顺序和一致性。
*/
